1. Introdução

Este relatório visa mostrar, por meio de uma espiral climática animada, a mudança das anomalias da temperatura média global ao longo dos anos. Com essa animação, será possível ver claramente o quão rica pode ser uma análise de série temporal, principalmente se soubermos usar as ferramentas corretas.
Quase todos os gráficos que vemos no dia a dia são estáticos, isto é, em termos gerais podemos dizer que são apenas uma simples imagem que nos dá informações úteis, mas muitas vezes grande parte do conhecimento que um gráfico poderia passar é perdido, já que não podemos ver sua evolução, apenas o resultado final, e isso é especialmente prejudicial em séries temporais. Com a animação criada, será possível visualizar não somente o ponto final, mas toda sua evolução. Em alguns segundos, teremos dados de 145 anos, ou 1740 meses!

Nas seções a seguir, serão detalhados os passos desde a escolha dos dados até o resultado final. A linguagem de programação utilizada foi R, devido sua intimidade com estatística e visualização de dados, além de pacotes gráficos específicos, além de outras ferramentas usadas para manipulação de dados.

A animação da espiral climática foi o formato escolhido por ser uma representação fidedigna, progressiva e já bem conceituada no mundo científico, originalmente proposta e criada por Ed Hawkins, do National Centre for Atmospheric Science da Universidade de Reading, no Reino Unido, em 2016.

2. Dados utilizados

Os dados utilizados neste trabalho são um dataset disponibilizado oficial e publicamente pela NASA (National Aeronautics and Space Administration), em suas dependências online, cujo download pode ser feito por este link. A base de dados nos dá informações acerca das anomalias de temperatura média globais desde 1880 até 2025, tendo como base de comparação a média dos 1951 a 1980, de modo que, como o planeta tem se tornado cada vez mais quente, é natural que nos anos anteriores a essa linha base, tenhamos tido “anomalias negativas”, em outras palavras, diferenças de temperatura média para baixo d

Abaixo, podemos ver as 12 últimas linhas do dataframe, referentes aos meses do ano de 2025.

Year Month Temperature Month number theta r
2025 Jan 1.38 1 0.0000000 2.30
2025 Feb 1.26 2 0.5235988 2.18
2025 Mar 1.36 3 1.0471976 2.28
2025 Apr 1.23 4 1.5707963 2.15
2025 May 1.08 5 2.0943951 2.00
2025 Jun 1.05 6 2.6179939 1.97
2025 Jul 1.02 7 3.1415927 1.94
2025 Aug 1.16 8 3.6651914 2.08
2025 Sep 1.25 9 4.1887902 2.17
2025 Oct 1.19 10 4.7123890 2.11
2025 Nov 1.21 11 5.2359878 2.13
2025 Dec 1.05 12 5.7595865 1.97

3. Descrição do código utilizado

Essa seção é divididas em duas sub-seções

  1. Tratamento dos dados, onde explicarei brevemente como os dados foram preparados.
  2. Lógica matemática, onde explicarei a lógica e método utilizados.
  3. Criação da animação, onde explicarei e detalharei partes cruciais do código.

Além disso, o código mostrado aqui contém a expressão “…caminho” nas partes de leitura de um dataset ou de salvamento do mesmo, de modo que o código possa ser apenas copiado e colocado em uso, de acordo com a organização e diretório necessários.

Tratamento dos dados

A limpeza e preparação dos dados foi extremamente simples, visto que o dataset disponibilizado pela NASA já estava muito bem organizado, de modo que foram feitas apenas algumas mudanças.

O primeiro pensamento de um estatístico ao receber um banco de dados para tratamento, deve ser sempre “Quais informações são importantes nesses dados, e o que posso eliminar ?”, de modo que primeiramente foram removidas variáveis que não seriam úteis para a análise em questão, como informações da temperatura por trimestre específico.

Além disso, o dataset inicialmente estava em formato largo, o que dificultaria a análise da série temporal, por isso foi necessária transformá-lo para um formato longo, por meio da função pivot_longer(), do pacote {tidyr}.

Os meses foram organizados em ordem correta, visto que havia o risco que o R colocá-los em ordem alfabética, e após isso, cada mês se tornou um ângulo específico.

Por último, as temperaturas se tornaram o valor do raio da espiral, e cada raio \(r_{i}\) foi calculado a partir da soma de cada temperatura a uma constante, garantindo que não tenhamos valores negativos, que acabariam colapsando no centro da espiral devido ao método matemático adotado, como será explicado a seguir

Abaixo, temos o código utilizado:

library(tidyr)
library(dplyr)

#IMPORTACAO
df_wide <- read.csv("...caminho/GLB.Ts+dSST.csv", skip = 1)
df_wide <- df_wide[-c(14:19)]
#WIDE TO LONG
meses <- colnames(df_wide[-1])
df_long <- df_wide %>% 
  pivot_longer(
    cols = meses,
    names_to = "Month",
    values_to = "Temp"
  )

#ORGANIZANDO MESES NA ORDEM CERTA
df_long <- df_long %>% 
  mutate(
    Month = factor(Month, levels = meses)
  )
#TRANSFORMANDO OS MESES EM ANGULOS
df_long <- df_long %>% 
  mutate(
    mes_numerico = as.numeric(Month),
    theta = 2*pi*(mes_numerico - 1)/12
  )
#GARANTINDO QUE NÃO TEREMOS VALORES NEGATIVOS EM COORDENADAS POLARES
minimo <- min(df_long$Temp)
const <- abs(minimo) + 0.1
#CRIANDO A VARIAVEL RAIO
df_long <- df_long %>% 
  mutate(
    r = Temp + const
  )

#SALVANDO O DATAFRAME
write.table(df_long, "...caminho/climate_dataframe.txt", sep = ",", row.names = F)

Lógica matemática

O pacote {gganimate} é basicamente uma extensão do {ggplot2}, de modo que antes de renderizarmos uma animação, é necessário criar um gráfico estático. Assim, ao criarmos o gráfico estático, precisamos pensar, de que modo posso criar uma espiral que fecha um ciclo a cada ano — passando por cada mês, passando por toda a série temporal ?

Se o gganimate é apenas uma extensão do ggplot, como existe uma espiral, e não uma circunferência exata ? A definição matemática é simples, o raio \(r\) depende não só do mês ou do ano, mas simultaneamente do par \((ano,\ mês)\), de modo que cada vez que a espiral fecha um ciclo, se inicia um novo ano. Assim, podemos notar a tendência de aumento das anomalias de temperatura conforme a espiral tende “se abrir” cada vez mais, isto é, a cada ciclo, o raio aumenta.

O lógica utilizada foi um método simples, mas bem conhecido no cálculo, chamado coordenadas polares. Em resumo, ele transforma números para um formato radial, a partir da seguinte fórmula:

\[ x = rcos(\theta) \newline y = rsen(\theta) \]

A partir dessa lógica, temos a temperatura transformada no raio r, como descrito anteriormente, e os meses sendo ângulos específicos, ângulos esses que foram calculados dividindo uma circunferência de \(12\pi\ rad\) em 12 partes, a partir da fórmula abaixo, de modo que cada \(\theta\) representa o ângulo de um mês:

\[ \theta_m = \frac{2\pi}{12}\times(m-1),\ m = 1, ..., 12 \]

O \(m-1\) na fórmula serve para fazer com que o primeiro mês, Janeiro, comece em 0 radianos, de modo que o último, Dezembro, não fique em \(2\pi\), causando uma sobreposição de Janeiro e Dezembro, visto que \(0\) e \(2\pi\) são equivalentes em coordenadas polares.

Vale comentar que usar esse método para evitar a sobreposição também cria outro problema adjacente, que é um vão entre Dezembro e Janeiro, visto que o ggplot2, mais especificamente, o geom_path não fecha o ciclo perfeitamente, já que em \(\theta = 2\pi\) teoricamente não havia nada, e para resolver isso, basta repetir o raio \(r_{Jan}\) ao final, garantindo continuidade sem redundância dos dados.

Criação da animação

Aqui, apresentarei um bloco de código e, logo abaixo, a devida explicação.

Importação dos dados e carregamento dos pacotes

dados <- read.table("...caminho/climate_dataframe.txt", header = T, sep = ",")

library(tidyverse)
library(ggplot2)
library(gganimate)
library(gifski)
library(viridis)

Aqui, apenas foram carregados os pacotes necessários, e o dataframe utilizado, que foi criado na parte de Tratamento dos dados!

Constante radial

const <- abs(min(dados$Temp)) + 0.1

media_51_80 <- dados %>%
  filter(Year >= 1951, Year <= 1980) %>%
  summarise(media = mean(Temp, na.rm = TRUE)) %>%
  pull(media)

r_media_51_80 <- media_51_80 + const

Definição de uma constante positiva que desloca levemente todas os valores, com o inuito de evitar valores negativos no raio.
Além disso, foi definida uma média dos anos 1951-1980 pare servir de base, que aparecerá como uma linha branca na animação.

Fechamento do ciclo anual

dados <- dados |>
  group_by(Year) |>
  bind_rows(
    dados |>
      filter(theta == 0) |>
      mutate(theta = 2*pi)
  ) |>
  ungroup()

Aqui, temos um bloco de código que serve para corrigir o erro apontado na parte matemática. Esse código replica os dados do mês de Janeiro de cada ano e reposiciona em \(\theta = 2\pi\).

Ângulos dos meses

meses_chave <- tibble(
  theta = 2*pi*((0:11) + 0.5)/12
)

Nessa parte, cada mês é convertido em um ângulo, como explicado anteriormente, a partir da fórmula descrita.

Preparação dos rótulos dos meses


nomes_meses_chave <- tibble(
  Month = c("Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"),
  theta = 2*pi*(0:11)/12,
  r = max(dados$r) + 0.2
)

Esse código apenas cria rótulos para os meses, que serão colocados ao redor da espiral. O raio é um pouco maior do que o valor próprio dele, sendo somado a 0.15m para garantir que o texto de cada mês não se sobreponha à própria espiral

Ordenação dos dados

dados <- dados %>% 
  arrange(Year, theta)

Como o nome sugere, apenas ordena os dados por ano e ângulo, para que o ggplot seja capaz de conectar os pontos corretamente ao plotar a série temporal.

Inicialização do ggplot

anim <- ggplot(dados, aes(
    x = theta,
    y = r,
    group = Year,
    colour = Year
    ))+

No começo do código do ggplot, definimos o eixo x como sendo o ângulo em radianos e o eixo y como o raio, e o agrupamento pela variável Year garante que cada ciclo seja um ano inteiro independentemente, e anos distintos não acabem se conectando de forma caótica

Trajetórias anuais

geom_path(alpha = 0.6, linewidth = 0.4) +

O geom_path() desenha as curva de cada ano na espiral, conectando os meses na ordem pré-definida, por causa do funcionamento dessa função é que foi necessário fazer a duplicata do mês de Janeiro, para que não sobrasse um vão. os argumentos alpha e linewidth serve, respectivamente, para controlar a opacidade e a espessura da linha.

Linha base para referência

geom_hline(yintercept = r_media_51_80,
             colour = "white",
             linewidth = 0.8,
             alpha = 0.8) +

Essa parte do código adiciona um “ciclo” constante, que é a média dos anos 1951 a 1980, para servir como referência visual para comparação.

Linhas dos meses

geom_segment(
    data = meses_chave,
    aes(
      x = theta,
      xend = theta,
      y = min(dados$r),
      yend = max(dados$r)
    ),
    inherit.aes = F,
    colour = "white",
    linewidth = 0.3,
    alpha = 0.4
  )+

Apenas um detalhe visual, mas que facilita bastante a visualização e interpretação. O código acima desenha linhas que delimitam a área que representa um mês e o separa dos meses vizinhos.

Rótulos dos meses

geom_text(
    data = nomes_meses_chave,
    aes(
      x = theta,
      y = r,
      label = Month
    ),
    inherit.aes = F,
    colour = "white",
    size = 4,
    fontface = "bold"
  )+

Aqui é a parte citada anteriormente, que adiciona os nomes dos meses ao redor da espiral, porém um pouco mais distante dela, o valor máximo dos dados somado a 0.2 (como visto na subseção ‘Preparação dos rótulos dos meses’!).

Conversão para coordenadas polares

coord_polar(start = -pi/12, direction = 1)+

Serve para transformar o sistema cartesiano em polar, convertendo os eixos comuns x e y em raio, o argumento start garante que Janeiro fique centrado logo acima, de modo que visualmente agradável e intuitivo.

Escala de cores

scale_colour_viridis_c(option = "magma",
                         begin = 0.1,
                         end = 0.95)+

Parte do código que visa tornar a animação inteligível para pessoas com daltonismo, visto que, em média, 1 a cada 20 pessoas tenha alguma dificuldade para dicernir cores.

Especificação do tema do ggplot

theme_void() +

Essa única linha pode alterar bastante o visual do gráfico, há várias opções possíveis, entretanto o tama ‘void’, que significa vazio, remove os eixos e outros elementos gráficos que, nesse caso, apenas poluiriam o o visual final, visto que neste caso não precisamos de uma leitura quantitativa exata, mas sim de uma representação abstrata ao longo do tempo.

Personalização

theme(
    plot.background = element_rect(fill = "black", color = NA),
    panel.background = element_rect(fill = "black", color = NA),
    plot.title = element_text(color = "white", size = 16, face = "bold"),
    plot.subtitle = element_text(color = "gray80", size = 11),
    legend.position = "right",
    legend.title = element_text(color = "white"),
    legend.text = element_text(color = "white"),
    plot.margin = margin(10, 10, 10, 10)
  )+

Apenas define estilos visuais do gráfico.

Títulos e início da animação

labs(
    title = "Anomalias de Temperatura Globais",
    subtitle = "Anomalias mensais da temperatura média global",
    colour = "Year"
  ) + transition_reveal(Year)

Define títulos e legenda. Além disso, a parte transition_reveal(Year) faz com que haja uma progressão, isto é, os dados sejam mostrados um após o outro, ordenados pelos anos, é isto que faz a animação da espiral fazer sentido.

A animação em si

animacao <- animate(anim,
        nframes = 450,
        fps = 30,
        width = 800,
        height = 800,
        renderer = gifski_renderer()
        )
anim_save("...caminho/espiral_clima.gif",
          animation = animacao)

Parte final da animação. É aqui que o {gganimate} transforma o gráfico estático do {ggplot} em algo dinâmico e progressivo. A função animate nos permite definir a quantidade total de quadros (nframes), sendo que quando maior esse, maior tende a ser a duração da animação resultante. Já fps nos permite definir a quantidade de quadros por segundo, que torna a animação mais fluida quanto maior for, mas ao mesmo tempo também gera um arquivo mais pesado.

5. Resultados

Abaixo, podemos ver a animação resultante, além do último frame da mesma.

Podemos ver claramente a progressão da série temporal conforme as cores mudam de mais frias para mais quentes, o que evidencia o aumento das anomalias de temperatura média globais.

6. Debugging e dificuldades

Como em todo trabalho que envolve código, sempre há erros de digitação (sintaxe) ou até mesmo de lógica, e houveram alguns especialmente importantes, que destaco abaixo

Rótulos dos meses

Inicialmente, a ideia era manter apenas 4 meses escritos ao redor da espiral, de forma trimestral, mas depois decidi manter todos os meses. Porém, em um sistema de coordenadas polares, \(0\) e \(2\pi\) representam o mesmo ângulo, por isso Janeiro e Dezembro ficaram sobrepostos, por isso resolvi o problema deslocando os meses em 0.5 (meio mês), separando os nomes sobrepostos.

Descontinuidade dos dados

Resolver o problema anterior gerou um novo infortúnio. Com os meses separados, existia um vão entre Dezembro e Janeiro! Um ausência completa de dados. Isso ocorreu porque o ggplot — mais especificamente, a função geom_path() liga um dado no próximo, porém como Janeiro começa em \(0\ rad\) , Dezembro termina em \(\frac{11\pi}{6}\ rad\), então a forma de resolver foi “duplicar” os dados de Janeiro de cada ano e colocar logo após Dezembro, em \(2\pi\), solucionando o problema. Foi uma confusão por parte do ggplot, que gerou a necessidade de uma adaptação na parte teórica do cálculo.

Erro na função de renderização

Um erro simples que eu normalmente não registraria aqui, mas tomou certo tempo até entender o que o estava causando. Ao renderizar a animação por meio da função animate do {gganimate}, centenas de imagens ficavam salvas no meu notebook, porém nenhuma animação era criada. Tentei alterar várias coisas, tanto com relação ao diretório, como a própria função. Tentei conseguir um diagnóstico usando class(animacao), e o resultado era “function”, sendo que deveria ser algo como “gif”. Apenas após bons minutos que encontrei o erro, e era uma simples falta de parênteses. Eu havia escrito gifski_renderer, e não gifski_renderer().

Posicionamento e cor do texto do gráfico

Aqui foi um problema principalmente estético. O fundo do gráfico foi definido como preto, porém o texto do Título e legenda também, por isso ficaram invisíveis, então a solução foi mudar a cor do texto.

Além disso, também houve complicações com posicionamento, visto que o título original havia ficado longo demais e vazado para fora da área do gráfico. Não tendo sido suficiente, criei uma confusão tentando acertar as dimensões do gráfico (para a esquerda e direita, e para cima e para baixo).

7. Possíveis melhorias

Aqui gostaria de deixar algumas ideias que poderiam ser realizadas se houvesse mais tempo para completar esse trabalho.

Gráfico 3d interativo

Como temos uma espiral em 2d cujo raio se expande, penso que teríamos algo próximo de um parabolóide em 3d, o que mostraria bem a progressão da série temporal. Abaixo podemos ver um protótipo.

Atualização automática

Uma ideia interessante seria organizar o código de modo que ele próprio atualizassa a URL da fonte dos dados importasse o dataset mais recente, criando assim uma nova animação sem precisar reescrever um código inteiro

Outras análises

Seria possível encontrar outros datasets com dados sobre a situação climática global e usá-los em paralelo com este, chegando assim a novas respostas e conclusões, além de gráficos e animações diferentes.

8. Referências

COOKSON, Alex. Building an animation step by step with gganimate. Disponível em: https://www.alexcookson.com/post/2020-10-18-building-an-animation-step-by-step-with-gganimate/. Acesso em: 29 jan. 2026.

NATIONAL AERONAUTICS AND SPACE ADMINISTRATION. GISTEMP: Global Surface Temperature Analysis. Disponível em: https://data.giss.nasa.gov/gistemp/. Acesso em: 29 jan. 2026.

NATIONAL AERONAUTICS AND SPACE ADMINISTRATION. Climate Spiral 1880–2022. Disponível em: https://science.nasa.gov/resource/video-climate-spiral-1880-2022/. Acesso em: 29 jan. 2026.

NUNES, Marcus. Daltonismo e paletas de cores inclusivas no R. Disponível em: https://marcusnunes.me/posts/daltonismo-paletas-de-cores-inclusivas-no-r/. Acesso em: 29 jan. 2026.

RANYDC. R Markdown themes. Disponível em: https://rpubs.com/ranydc/rmarkdown_themes. Acesso em: 29 jan. 2026.